/* Copyright © 2023 - 2024 Coremail论客
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import socket from '@ohos.net.socket';
import wifiManager from '@ohos.wifiManager';
import connection from '@ohos.net.connection';

import { findBuffer, joinBuffers } from '../utils/buffer_utils';
import { getLogger, isDebugLog } from '../utils/log';
import type { Logger } from '../utils/log';
import { delay, EventHandler } from '../utils/common';
import { decodeCharset } from '../utils/encodings';
import { CMError, ErrorCode, IBuffer } from '../api';
import { MemBuffer } from '../utils/file_stream';

export enum NetworkState {
  CONNECTING = 0,
  CONNECTED = 1,
  DISCONNECTED = 2,
  ERROR = 3,
}

type ConnectionOption = {
  timeout: number;
};

function int2ip(n: number): string {
  return `${(n & 0xff000000) >>> 24}.${(n & 0x00ff0000) >>> 16}.${
  (n & 0x0000ff00) >>> 8
  }.${n & 0xff}`;
}

async function resolveHost(host: string): Promise<string> {
  const handler = await connection.getDefaultNet();
  let netAddress = await handler.getAddressesByName(this.host);
  let ip: string;
  for (let i = 0; i < netAddress.length; i++) {
    if (isIP(netAddress[i].address)) {
      ip = netAddress[i].address;
      break;
    }
  }
  if (!ip) {
    return Promise.reject(new CMError("no available ip", ErrorCode.PARAMETER_ERROR));
  }
  return ip;
}

function isIP(str: string): boolean {
  if (str.length < 7 || str.length > 15) {
    return false;
  }

  var arr: string[] = str.split(".");
  if (arr.length != 4) {
    return false;
  }

  for (let i = 0; i < 4; i++) {
    var s = arr[i];
    for (let j = 0; j < s.length; j++) {
      if (s.charAt(j) < "0" || s.charAt(j) > "9") {
        return false;
      }
    }
  }

  for (let i = 0; i < 4; i++) {
    var temp = parseInt(arr[i]);
    if (temp < 0 || temp > 255) {
      return false;
    }
  }
  return true;
}

function getLocalIp(): string {
  const ipInfo = wifiManager.getIpInfo();
  const localHost = int2ip(ipInfo.ipAddress);
  return localHost;
}

export class Connection extends EventHandler {
  host: string;
  port: number;
  isSSL: boolean;
  sock?: socket.TCPSocket | socket.TLSSocket;
  ca: string[] = []

  option: ConnectionOption = {
    timeout: 10000,
  };

  bufferList: Uint8Array[] = [];
  byteLength: number = 0;

  state = NetworkState.DISCONNECTED;
  connectedPromise?: Promise<void>;
  logger: Logger;

  constructor(
    host: string,
    port: number,
    isSSL: boolean,
    ca?: string[],
    option?: ConnectionOption
  ) {
    super();
    this.logger = getLogger("protocol");
    this.logger.addPrefix(host + ":" + port.toString());
    this.host = host;
    this.port = port;
    this.isSSL = isSSL;
    if (option && option.timeout) {
      this.option.timeout = option.timeout
    }
    if (ca) {
      this.ca = ca;
    }
  }

  isConnected(): boolean {
    return this.state == NetworkState.CONNECTED;
  }

  async connect(): Promise<void> {
    const logger = this.logger;
    logger.info("connect", this.host, this.port, this.isSSL);
    let sock: socket.TCPSocket | socket.TLSSocket;
    if (this.isSSL) {
      sock = socket.constructTLSSocketInstance();
      await sock.bind({ address: 'localhost', family: 1, })
    } else {
      sock = socket.constructTCPSocketInstance();
    }
    this.sock = sock;
    if (!this.connectedPromise) {
      this.connectedPromise = new Promise<void>((resolve, reject) => {
        const dataHandler = ({ message }) => {
          this.processData(message);
        }
        const cleanSock = () => {
          sock.off('message');
          this.state = NetworkState.DISCONNECTED;
          this.connectedPromise = undefined;
        }
        sock.on("message", dataHandler);
        sock.on("close", () => {
          logger.info("close");
          cleanSock();
        });
        sock.on("connect", () => {
          logger.info("connected");
          this.state = NetworkState.CONNECTED;
          resolve();
        });
        sock.on("error", (err: Error) => {
          logger.error("onerror", err);
          sock.close();
          cleanSock();
          reject(err);
        });
        let success = false;
        this._connect(sock).then(() => {
          success = true;
        }).catch(err => {
          this.logger.error('connect failed', JSON.stringify(err));
          cleanSock();
          reject(new CMError("connect failed", ErrorCode.CONNECT_FAILED, err));
        });
        delay(this.option.timeout).then(() => {
          if (success) {
            return;
          }
          this.logger.error('connect timeout');
          sock.close();
          cleanSock();
          reject(new CMError("connect timeout", ErrorCode.CONNECT_TIMEOUT));
        })
      });
    }
    return this.connectedPromise;
  }

  async _connect(sock: socket.TCPSocket | socket.TLSSocket) {
    const option = {
      address: {
        address: this.host,
        port: this.port
      }
    }
    if (this.isSSL) {
      const sOption = {
        ...option,
        secureOptions: {
          ca: this.ca,
          protocols: [socket.Protocol.TLSv12, socket.Protocol.TLSv13],
          // useRemoteCipherPrefer: true,
        },
      }
      await (sock as socket.TLSSocket).connect(sOption);
    } else {
      await (sock as socket.TCPSocket).connect(option);
    }
    this.sock = sock;
  }

  startTLS(): void {
    if (this.isSSL) {
      return;
    }
  }

  processData(data: ArrayBuffer): void {
    const u8a = new Uint8Array(data);
    if (isDebugLog()) {
      const s = decodeCharset(u8a).substring(0, 2048);
      this.logger.trace('processData', s.replace(/\r\n/g, '\\r\\n'));
      this.logger.debug(
        "processData",
        data.byteLength,
        this.byteLength,
        this.bufferList.length,
        // decodeCharset(new Uint8Array(data)).replace(/\r\n/g, "\\r\\n"),
      );
    }
    this.bufferList.push(u8a);
    this.byteLength += data.byteLength;
    this.emit("data");
  }

  async send(data: string | Uint8Array | IBuffer): Promise<void> {
    if (typeof data == 'string' || data instanceof Uint8Array) {
      await this._send(data);
    } else if (data instanceof MemBuffer) {
      for (const chunk of data.data) {
        await this._send(chunk);
      }
    } else {
      for await (const chunk of data.readRaw()) {
        this._send(chunk);
      }
    }
  }

  async _send(data: string | Uint8Array): Promise<void> {
    const logger = this.logger;
    if (isDebugLog()) {
      if (typeof data == 'string') {
        logger.debug("send", data.replace(/\r\n/g, "\\r\\n"));
      } else {
        logger.debug("send bytes", Array.from(new Uint8Array(data)));
      }
    }
    if (!this.sock || !this.isConnected()) {
      logger.warn('send data when no connection', this.sock, this.state);
      return;
    }
    let sendingData: string | ArrayBuffer;

    if (typeof data == 'string') {
      sendingData = data;
    } else if (data.byteOffset == 0 && data.byteLength == data.buffer.byteLength) {
      sendingData = data.buffer;
    } else {
      const buff = new Uint8Array(data.byteLength);
      buff.set(data);
      sendingData = buff.buffer;
    }
    try {
      if (this.isSSL) {
        await (this.sock as socket.TLSSocket).send(sendingData);
      } else {
        await (this.sock as socket.TCPSocket).send({ data: sendingData });
      }
    } catch (e: unknown) {
      logger.error('send data failed', JSON.stringify(e));
      throw new CMError("send data failed", ErrorCode.SEND_DATA_FAILED, e as Object);
    }
  }

  resetData() {
    this.logger.info("reset data")
    this.bufferList = [];
    this.byteLength = 0;
  }

  async fetchData(timeout?: number): Promise<Uint8Array | undefined> {
    // this.logger.debug("fetchData", this.bufferList.length, this.byteLength);
    let buff = this.bufferList.shift();
    if (!buff) {
      timeout = (timeout || this.option.timeout);
      const endTime = (new Date).getTime() + timeout;
      for (let i = 0; i < 6000; i++) {
        await delay(10);
        if (this.bufferList.length > 0) {
          break;
        }
        if (this.state != NetworkState.CONNECTED) {
          const error = new CMError("fetchData disconnect", ErrorCode.CONNECT_FAILED);
          return Promise.reject(error);
        }
        if ((new Date).getTime() > endTime) {
          this.logger.debug("fetch timeout", timeout, '.');
          return undefined;
        }
      }
      this.logger.debug("fetchData 2", this.bufferList.length, this.byteLength);
      buff = this.bufferList.shift();
    }
    return buff;
  }

  async getMaxData(max?: number, timeout?: number): Promise<Uint8Array | undefined> {
    // this.logger.debug("getMaxData", max);
    let buff = await this.fetchData(timeout);
    if (!buff) {
      return undefined;
    }
    if (max && max < buff.byteLength) {
      this.logger.debug('split data', max, buff.byteLength);
      const remain = buff.subarray(max);
      buff = buff.subarray(0, max);
      this.bufferList.unshift(remain);
    }
    this.byteLength -= buff.byteLength;
    return buff;
  }

  async getDataUntil(end: Uint8Array, timeout?: number): Promise<Uint8Array> {
    // this.logger.debug("getDataUntil", end);
    let buffer: Uint8Array;
    let offset = 0;
    while (true) {
      let buff = await this.fetchData(timeout);
      if (!buff) {
        const error = new CMError("fetchData timeout", ErrorCode.PROTOCOL_TIMEOUT);
        return Promise.reject(error);
      }
      if (!buffer) {
        buffer = buff;
      } else {
        const newBuffer = new Uint8Array(buffer.byteLength + buff.byteLength);
        newBuffer.set(buffer);
        newBuffer.set(buff, buffer.byteLength);
        offset = buffer.byteLength - end.byteLength;
        if (offset < 0) {
          offset = 0;
        }
        buffer = newBuffer;
      }
      const idx = findBuffer(buffer, end, offset);
      if (idx >= 0) {
        const result = buffer.subarray(0, idx);
        const remain = buffer.subarray(idx + end.byteLength);
        if (remain.byteLength > 0) {
          this.logger.debug('remain data', remain.byteLength);
          this.bufferList.unshift(remain);
        }
        this.byteLength -= result.byteLength + end.byteLength;
        return result;
      }
    }
  }

  close(): void {
    this.state = NetworkState.DISCONNECTED;
    this.sock?.close();
  }
}
